const serialize = function(content) {

    // return JSON.stringify(content);
    return @.serialize([content]);

};

const deserialize = function(content) {

    return @.deserialize(content)[0];

};

const createSession = function(redisClient, id, hashID, hashVersionID, timeout, keys_can_delete) {

    const record = {
        "raw": Object.create(null),
        "serialized": Object.create(null)
    };

    const update = function(key, value) {

        if (@.is.nil(value)) {
            delete record.raw[key];
        } else {
            record.raw[key] = value;
        }

        let serialized = serialize(value);

        @.async(function() {

            redisClient.execute("hmset", hashID, key, serialized).pipe(this);

        }).then(function() {

            record.serialized[key] = serialized;

            redisClient.execute("pexpire", hashID, timeout).pipe(this);

        }).then(function() {

            redisClient.execute("incr", hashVersionID).then(function(version) {

                record.version = version + "";

                this.next();

            }).pipe(this);

        }).then(function() {

            redisClient.execute("pexpire", hashVersionID, timeout).pipe(this);

        }).then(function() {

            @.async.all(keys_can_delete, function(key) {
                let key_name = key.split(":")[0]
                let key_id = key_name.split(".").reduce((target, k) => {
                    return target && target[k] ? target[k] : null
                }, record.raw)
                if (key_id) {
                    let map_key = key_name + ":" + key_id
                    redisClient.execute("get", map_key).then(function(sids) {
                        let sidString = ""
                        if (sids) {
                            let sidArray = sids.split(",")
                            if (sidArray.indexOf(id) < 0) {
                                sidArray.push(id)
                            }
                            sidString = sidArray.join(",")
                        } else {
                            sidString = id
                        }
                        redisClient.execute("set", map_key, sidString).then(function() {
                            redisClient.execute("pexpire", map_key, timeout).pipe(this);
                        }).pipe(this);
                    }).pipe(this);
                } else {
                    this.next()
                }
            })

        }).rejected(function(error) {

            @error(`Failed update redis session[${id}].${key}`);
            @error(error);

        });

    };

    record.session = new Proxy(record.raw, {
        "set": function(target, key, value, receiver) {
            update(key, value);
        },
        "deleteProperty": function(target, key) {
            update(key, undefined);
        }
    });

    return record;

};

module.exports = function(connection, options) {

    const sessionPrefix = options.prefix || "session";
    const cookieKey = options.key || "MewSID";

    const keys_can_delete = options.keys_can_delete || []

    const timeout = options.timeout || 20 * 60 * 1000;
    const sessionPool = @.mem.cachePool(timeout);

    const redisClient = connection.redis.client;

    return function(request, response) {

        let sid = request.cookies[cookieKey];
        if (!sid) {
            sid = @.uuid();
            response.cookies[cookieKey] = sid;
        }

        let hashID = `${sessionPrefix}:${sid}`;
        let hashVersionID = `${hashID}:version`;

        let record = sessionPool.get(sid, true, () => {
            return createSession(redisClient, sid, hashID, hashVersionID, timeout, keys_can_delete);
        });

        @heard("lab.session.delete").then(function(mew) {
            let { key, id } = mew.content["lab.session.delete"]
            if (key && id) {
                let key_name = key.split(":")[0]
                let map_key = key_name + ":" + id
                redisClient.execute("get", map_key).then(function(sids) {
                    if (sids) {
                        return @.async.all(sids.split(","), function(sid) {
                            let sessionID = `${sessionPrefix}:${sid}`;
                            let sessionVersionID = `${sessionID}:version`;
                            let record = sessionPool.get(sid, true, function() {
                                return createSession(redisClient, sid, sessionID, sessionVersionID, timeout, keys_can_delete);
                            });
                            key.split(":")[1].split(",").forEach(function(sessionKey) {
                                if (record.session[sessionKey]) {
                                    delete record.session[sessionKey]
                                }
                            })
                            redisClient.execute("del", map_key).pipe(this)
                        })
                    } else {
                        this.next()
                    }
                })
            }
        })

        return redisClient.execute("get", hashVersionID).then(function(serverVersion) {

            if (record.version === serverVersion + "") {
                this.next();
                return;
            }

            if (!serverVersion) {

                @.async(function() {

                    let msets = [];
                    for (let key in record.raw) {
                        msets.push(key);
                        msets.push(serialize(record.raw[key]));
                    }

                    if (msets.length > 0) {
                        @debug(`Restoring session[${sid}]`);
                        redisClient.execute.apply(redisClient, ["hmset", hashID].concat(msets)).pipe(this);
                    } else {
                        @debug(`Creating session[${sid}]`);
                        this.next();
                    }

                }).then(function() {

                    redisClient.execute("incr", hashVersionID).then(function(version) {
                        @debug(`Updating session[${sid}] version: ${version}`);
                        record.version = version + "";
                        this.next();
                    }).pipe(this);

                }).pipe(this);

                return;
            }

            @debug(`Updating session[${sid}] version: ${serverVersion}`);
            redisClient.execute("hgetall", hashID).then(function(data) {

                record.version = serverVersion + "";

                let looper = 0;
                while (looper < data.length) {
                    let key = data[looper];
                    let serialized = data[looper + 1];
                    if (key && serialized && (serialized !== record.serialized[key])) {
                        try {
                            record.serialized[key] = serialized;
                            let deserialized = deserialize(data[looper + 1]);
                            if (deserialized !== null) {
                                record.raw[key] = deserialized;
                            } else {
                                delete record.raw[key];
                            }
                        } catch (error) {
                            @error(`Failed to parse session.${data[looper]}: ${data[looper + 1]}`);
                        }
                    }
                    looper += 2;
                }

                this.next(record.session);

            }).pipe(this);

        }).then(function() {

            redisClient.execute("pexpire", hashID, timeout).pipe(this);

        }).then(function() {

            redisClient.execute("pexpire", hashVersionID, timeout).pipe(this);

        }).then(function() {

            this.next(record.session);

        }).rejected(function(error) {

            @error(error);

        });
    }
}